Function calling en OpenAI: estructurando salidas del modelo

Diagrama de funciones conectadas con flechas representando flujo de datos

OpenAI introdujo function calling en GPT-3.5-turbo y GPT-4 en junio de 2023. Es una de las features que más cambia cómo se integra un LLM en una aplicación: en lugar de parsear texto libre y rezar, el modelo te devuelve directamente JSON estructurado que puedes ejecutar. Cubrimos cómo funciona, qué patrones se han consolidado en estos meses, y los errores típicos que se ven en producción.

El problema que resuelve

Antes de function calling, integrar GPT con un sistema externo seguía un patrón frágil:

  1. Pides al modelo que responda en JSON con un schema concreto.
  2. Aplicas regex o un parser robusto a la salida.
  3. Si el modelo se desvía mínimamente (un comentario fuera del JSON, una coma extra), todo falla.
  4. Pones reintentos con prompts cada vez más estrictos.

Function calling formaliza este patrón. Declaras explícitamente las “funciones” disponibles y sus parámetros usando JSON Schema. El modelo decide cuándo invocar una y devuelve un objeto que respeta el schema. El parsing es trivial.

Cómo se declara

Una función se describe con tres campos:

{
  "name": "get_weather",
  "description": "Obtiene el tiempo actual en una ciudad",
  "parameters": {
    "type": "object",
    "properties": {
      "city":   {"type": "string", "description": "Ciudad, p.ej. 'Madrid'"},
      "units":  {"type": "string", "enum": ["celsius", "fahrenheit"]}
    },
    "required": ["city"]
  }
}

Pasas la lista de funciones disponibles junto con el mensaje del usuario:

response = openai.ChatCompletion.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "¿Qué tiempo hace en Madrid?"}],
    functions=[get_weather_function],
    function_call="auto",
)

La respuesta puede ser un mensaje normal, o un objeto con function_call que contiene name y arguments (un string JSON con los parámetros). Tu código recibe el JSON, ejecuta la función real, y opcionalmente devuelve el resultado al modelo para que componga la respuesta final.

Casos de uso que han madurado

En los meses desde el lanzamiento, han emergido patrones claros:

  • Agentes simples. Un modelo que puede invocar herramientas (búsqueda web, calculadora, base de datos) cuando lo necesita.
  • Extracción estructurada. Convertir texto libre (correo, llamada transcrita, formulario sin estructura) en datos tipados — un caso donde antes se usaba regex frágil o NER tradicional.
  • APIs de conversación tipo “asistente del producto”. El usuario pide en lenguaje natural; el modelo decide qué endpoint backend invocar.
  • Routing de consultas. Decidir entre varios pipelines (FAQ, ticket de soporte, escalado humano) basándose en la naturaleza de la pregunta.
  • Generación de código que llama APIs internas. El modelo “compone” llamadas a funciones del sistema dado un objetivo de alto nivel.

El patrón ReAct con function calling

El loop que repiten todos los frameworks de agentes (LangChain, LlamaIndex, AutoGen) en su forma esencial:

1. Usuario hace pregunta
2. Modelo recibe la pregunta + lista de funciones
3. Modelo decide: responde directamente o invoca función
4. Si invoca: ejecutar función real
5. Pasar resultado al modelo
6. Modelo decide otra vez: nueva función o respuesta final
7. Repetir hasta que el modelo dé respuesta final

Function calling lo hace robusto. Sin él, era un castillo de naipes basado en parseo de texto.

Errores comunes que vemos en producción

Después de meses de uso, los problemas que aparecen una y otra vez:

  • Schemas demasiado permisivos. Si declaras un parámetro como string sin enum ni descripción precisa, el modelo improvisa. Sé estricto con tipos y enumeraciones.
  • Descripciones pobres. La description de la función y de cada parámetro es lo que el modelo lee para decidir cuándo invocarla. Es prompt, no documentación. Inviértele tiempo.
  • Demasiadas funciones a la vez. Más de 10-15 funciones simultáneas degrada la decisión del modelo. Considera categorización o un primer paso de routing.
  • Confiar en que la función se invoca siempre. El modelo puede decidir responder directamente. Maneja ambos casos.
  • No validar los argumentos. El modelo se ajusta al schema en >95% de los casos pero no siempre. Valida antes de ejecutar — sobre todo si la función tiene side effects (borrar datos, enviar email).
  • Bucles infinitos en loops de agente. Sin límite de iteraciones, un agente confundido puede llamarse a sí mismo eternamente. Pon max_iterations.

Coste y latencia

Function calling tiene coste extra que conviene conocer:

  • Tokens consumidos: la declaración de funciones se incluye en cada llamada — ocupa input tokens. Un schema grande puede sumar 500-1000 tokens por petición.
  • Latencia de la cadena: cada loop de “modelo → función → modelo” añade roundtrips. Un agente que requiere 4 pasos tarda 4× más que una respuesta directa.
  • Cacheo: las funciones declaradas no cambian entre llamadas; con prompt caching (cuando esté disponible para tu modelo) el coste se amortiza.

Conclusión

Function calling es una de las mejores adiciones recientes a la API de OpenAI. Resuelve un problema real (parsear salida de LLM) con una abstracción correcta. Para cualquier proyecto que integre GPT con sistemas que esperan datos estructurados, es la opción por defecto. Los patrones de agentes que se construyen sobre esto van a definir cómo se construye software con IA en los próximos años.

Síguenos en jacar.es para más sobre integración de LLMs y herramientas para construir productos con IA.

Entradas relacionadas